Object.prototype.like = '舌尖上的JS'
請在任意的 JavaScript 環境輸入以上程式碼,然後隨意宣告一個變數、陣列或函式,甚至一個空物件都可以 let name = {}
,然後在變數後加上 .like
看看印出什麼
神奇的傑克,每個變數都突然有個 like 屬性是 舌尖上的JS
,這不是魔法,是 JavaScript 透過原型做到繼承的效果!
這篇文帶你認識 JavaScript 中這個圓圓的東西,包括:
還記得之前的雞蛋糕比喻,透過 new + constructor 創造物件嗎? 再看一次程式碼
生成的兩個雞蛋糕 rabbit1、rabbit2 確實都依照 RabbitCake constructor 的設定各自擁有三個屬性,修改 rabbit1 中的 shape
屬性並不會影響 rabbit2,因為他們是各自獨立的。
function RabbitCake(flavor) {
this.producer = 'Hooo';
this.shape = 'rabbit';
this.flavor = flavor;
}
let rabbit1 = new RabbitCake('cream');
let rabbit2 = new RabbitCake('chocolate');
rabbit1.shape = 'bunny'
console.log(rabbit1.shape) // bunny
console.log(rabbit2.shape) // rabbit
若是希望將所有的兔子雞蛋糕都加上一個製造日期的屬性,可以怎麼做呢?
難道再去修改 RabbitCake 內加上 this.productionDate
嗎,不不不,一來已經創出來的物件並不會因此多出這個屬性,二來屬性還是獨立存在在新物件內,並沒有達到共用的目的。
解決辦法就是 - 將共享的屬性和方法放入 原型 prototype 中。
來看看 ECMA 針對 prototype 的說明, 原型 prototype 是個存在於所有 constructor 內的一個物件屬性,用於實現基於原型的繼承與屬性共享。
4.3.1 Objects
Each constructor is a function that has a property named "prototype" that is used to implement prototype-based inheritance and shared properties.4.4.8 prototype
object that provides shared properties for other objects
NOTE
When a constructor creates an object, that object implicitly references the constructor's "prototype" property for the purpose of resolving property references. The constructor's "prototype" property can be referenced by the program expression constructor.prototype, and properties added to an object's prototype are shared, through inheritance, by all objects sharing the prototype. Alternatively, a new object may be created with an explicitly specified prototype by using the Object.create built-in function.
有了 prototype 這個物件,將所有共享的屬性放入 RabbitCake 的 prototype 中,之後創出來的所有兔子雞蛋糕都可以共享屬性了!
function RabbitCake(flavor) {
this.flavor = flavor;
}
// 將共享的屬性放入 prototype 物件中
RabbitCake.prototype = {
producer: 'Hooo',
shape: 'rabbit',
productionDate: 'Oct.1, 2021'
}
let rabbit1 = new RabbitCake('cream');
let rabbit2 = new RabbitCake('chocolate');
console.log(rabbit1.productionDate) // 'Oct.1, 2021'
console.log(rabbit2.productionDate) // 'Oct.1, 2021'
上一篇物件導向中提到,與 class-based 語言不同,prototype-based 的語言透過原型的操作可以動態的添加或刪除屬性,可在任何時間不受限在定義時,這什麼意思呢?
剛剛創造出的 rabbit1、rabbit2 兩個實例成功繼承了 RabbitCake.prototype 內的屬性,那若我們修改了 RabbitCake.prototype 的內容會受影響嗎?
看看以下示範:
// 刪除 shape 屬性
delete RabbitCake.prototype.productionDate
// 修改 producer 屬性
RabbitCake.prototype.producer: 'HooGoodGood',
// 共享的屬性修改也影響實例 instance
console.log( rabbit1.productionDate) // undefined
console.log( rabbit1.producer ) // HooGoodGood
答案是:會的!
由於這些屬性並非存在於 實例 instance 中,而是透過查訪屬性所在的 prototype
,所以當然會跟著變動,那又是怎麼查訪的呢? 透過一條名為原型鏈的線索!
ECMA 的原型鏈定義:
Every object created by a constructor has an implicit reference (called the object's prototype) to the value of its constructor's "prototype" property.
Furthermore, a prototype may have a non-null implicit reference to its prototype, and so on; this is called the prototype chain.
每個由 constructor 創出來的物件都有個隱性的指標,指向了 constructor 的 prototype,並且,constructor 也會有一個隱性指標再指向創造他的 constructor prototype內,就這樣一直指下去,最終連結到 Object.prototype,就稱為原型鏈。
所以雖然 producer 和 shape 不存在 rabbit1 和 rabbi2 內,但可以透過原型鏈往上查找到 RabbitCake 的 prototype,這些隱性的指標指向了 constructor 的 prototype,每個創出來的實例 instance (那些雞蛋糕),都可以成功取得原型鏈上的所有屬性。
使用 __proto__
可以在原型鏈上一層一層往上查訪 prototype
rabbit1.__proto__ === RabbitCake.prototype
Rabbit.prototype.__proto__ === Object.prototype
Object.prototype.__proto__ === null
// 直接從 rabbit1 連到原型鏈末端
rabbit1.__proto__.__proto__.__proto__ === null
// RabbitCake 本身是 function,也可以往上查找到 Function constructor
RabbitCake.__proto__ === Function.prototype
Function.prototype.__proto__ === Object.prototype
所有原型鏈最起始的原型都是 Object.prototype
,而 Object.prototype.__proto__
是 null
,這就是原型鏈的終點。
也可以使用 Object.getPrototypeOf()
回傳括號內物件的原型
Object.getPrototypeOf(rabbit1) === RabbitCake.prototype
Object.getPrototypeOf(RabbitCake.prototype) === Object.prototype
Object.getPrototypeOf(Object.prototype) === null
如果想知道一個屬性是存在該 實例instance 本身,還是繼承自原型鍊上,可以用 hasOwnProperty
確認 true or false
rabbit1.hasOwnProperty('flavor') // true
rabbit1.hasOwnProperty('producer') // false,繼承自 CakeRabbit prototype
rabbit1.hasOwnProperty('shape') // false,繼承自 CakeRabbit prototype
透過 X instanceof Y
來判斷 X 是否為 Y 的 實例 instance
rabbit1 instanceof RabbitCake // true
rabbit1 instanceof Function // false,rabbi1 的 constructor 是 RabbitCake 不是 Function
RabbitCake instanceof Function // true
Function instanceof Object // true
Object instanceof Function // true,有趣的是 Object 與 Function 互為 constructor
來解答昨天和今天的答案吧
Q1: 在查詢 MDN 語法關於 string、array、object 的內建方法時,發現開頭都有 prototype 這個字樣,可是明明在使用上沒有呀!
// MDN 語法
Array.prototype.map()
// 實際使用
['Ritz', 'Lotus', 'Oreo'].filter(chocolate => chocolate.length > 4)
答:有沒有發現這些內建方法都是寫在 Array.prototype 下呢,大寫的 Array 就是一個內建的 constructor,任何的陣列都是這個 Array constructor 的 實例 instance,所以寫在 Array prototype 下的方法都能透過原型鏈繼承給任何陣列,如同文中的 rabbit1 要取得 producer 一樣,直接使用 [].map 就可以使用囉!
(想當初菜鳥時百思不得其解,現在看完原型鏈的解釋 4 不 4 小菜一碟呢 )
Q2: 為什麼任何的變數都能印出 舌尖上的JS
Object.prototype.like = '舌尖上的JS'
答:由於我將 like 這個屬性寫在 Object.prototype 內,而JavaScript 中任何的實例 instance 都能在原型鏈中查訪到 Object.prototype,所以不論是哪種型別的變數一定都能找到這個屬性!
若是寫在 Array.prototype.like = '舌尖上的JS'
就只有陣列可以印出這個 like 屬性
好的,我要來自首,開頭的 Object.prototype.like = '舌尖上的JS'
是個不良示範,在程式中盡可能 「不要去修改預設的物件原型」,不去修改不屬於你建立的物件 Don’t modify objects you don’t own
Object.prototype 的示範僅作為 prototype 與繼承概念,實務上若變動此 prototype 容易造成非預期的變動。
有個名詞稱為 prototype pollution 原型污染,就是惡意的使用者利用了原型鏈的設計,在其中的原型中埋入有害的程式碼,造成整個原型鏈的污染,剛好這幾天 Huli 大大發表一篇相關文章 基於 JS 原型鏈的攻擊手法:Prototype Pollution ,資安也是在學習程式碼中必備的觀念,推薦可以往這部分深究!
ECMA
MDN instanceof
該來理解 JavaScript 的原型鍊了
從ES6 開始的JavaScript 學習生活
从设计初衷解释 JavaScript 原型链
Javascripter 必須知道的繼承 prototype, prototype, proto
JavaScript Prototype
你懂 JavaScript 嗎?#19 原型(Prototype)
基於 JS 原型鏈的攻擊手法:Prototype Pollution
The Complete Guide to Prototype Pollution Vulnerabilities
Don’t modify objects you don’t own
圓圓的原型篇結束!一知半解的部分在整理文章的過程得到解答,希望這篇文章對於初探原型的你有所幫助,Reference 附上在研究時幫助我許多的文章,分享給你們~